读取与导出本地图片

效果

  1. 通过代码读取本地的 png 图片(可自己扩展其它类型)转换为 UE4 内部可用的 Texture2D;

  2. 利用 UE4 内的 Texture2D 在本地指定目录下生成对应 png 图片;

  3. 利用 UE4 内的 Sprite 在本地指定目录下生成对应的 png 图片。

PNG -> Texture2D

分析

  1. 读取指定目录的 png 图片,将信息存入数组 RawFileData
  2. 用 ImageWrapper 来保存 RawFileData 转为可用信息
  3. 生成空的 Texture
  4. 将数据写入 Texture 中
  5. 保存资源

读取指定目录的 png 图片,将信息存入数组 RawFileData

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const FString PicturePath = ImageDirectory.Path + TEXT("/") + ImageName + TEXT(".png");

if (!FPlatformFileManager::Get().GetPlatformFile().FileExists(*PicturePath))
{
// 不存在 PicturePath 文件
return nullptr;
}

TArray<uint8> RawFileData;
if (!FFileHelper::LoadFileToArray(RawFileData, *PicturePath))
{
// 无法解析 PicturePath
return nullptr;
}

用 ImageWrapper 来保存 RawFileData 转为可用信息

需要依赖模块:ImageWrapper ,包含头文件:IImageWrapper.hIImageWrapperModule.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
IImageWrapperModule& ImageWrapperModule = FModuleManager::LoadModuleChecked<IImageWrapperModule>("ImageWrapper");

TSharedPtr<IImageWrapper> ImageWrapper = ImageWrapperModule.CreateImageWrapper(EImageFormat::PNG);

if (ImageWrapper.IsValid() && ImageWrapper->SetCompressed(RawFileData.GetData(), RawFileData.Num()))
{
const TArray<uint8>* UncompressedRGBA = nullptr;

// ERGBFormat::BGRA 为 RGB 的格式
// 第二个参数为通道的位深度,通常为 8
// UncompressedRGBA 保存了uncompressed raw data.
if (ImageWrapper->GetRaw(ERGBFormat::BGRA, 8, UncompressedRGBA))
{
/* 执行接下来的操作 */
}
}

生成空的 Texture

首先我们要确定想要生成的 Texture 所在的位置,也就是要确定 Package 的位置,也就是确定 Path:

image-20210812142156093.png

Package 的位置由 MountPoint 开始,这个 MountPoint 就是类似 /Game/Engine 这样的东西;

如果我们需要 在指定目录保存这些文件,比如我们不想在 /Game/Engine 的目录写入这些信息,想在自己的插件目录、其它本地目录保存,那么我们需要先注册 MountPoint:

1
2
// Register mount point to save assets
FPackageName::RegisterMountPoint("/你想的 MountPoint 名字/", 对应的路径);

接着我们:

1
2
3
4
5
6
const FString PackageName = MountPointName + TEXT("/textures/") + ImageName;

UPackage* TexturePackage = CreatePackage(nullptr, *PackageName);
TexturePackage->FullyLoad();

Texture = NewObject<UTexture2D>(TexturePackage, FName(*ImageName), RF_Public | RF_Standalone | RF_MarkAsRootSet);

将数据写入 Texture 中

我们生成的空 Texture 会默认采用 Mipmap 压缩,但是显然这不一定合理。

如果 边长 不是 2 的幂次方,则不能使用 Mipmap 压缩,否则会出现错误,我们可以用 x & (x - 1) 来判断 x 是否是 2 的幂次方,如果 x & (x - 1) == 0 则说明是,否则不是 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
int32 SizeX = ImageWrapper->GetWidth();
int32 SizeY = ImageWrapper->GetHeight();
const EPixelFormat PixelFormat = PF_B8G8R8A8;
int32 PixelSize = SizeX * SizeY * GPixelFormats[PixelFormat].BlockBytes;
// PixelSize 计算了所有的像素信息一共占了多少空间

if (SizeX > 0 && SizeY > 0 && (SizeX % GPixelFormats[PixelFormat].BlockSizeX) == 0 && (SizeY % GPixelFormats[PixelFormat].BlockSizeY) == 0)
{
/* 先生成一个空的 Texture */

// 更新 Texture 的 PlatformData
Texture->PlatformData = new FTexturePlatformData();
Texture->PlatformData->SizeX = SizeX;
Texture->PlatformData->SizeY = SizeY;
Texture->PlatformData->NumSlices = 1;
Texture->PlatformData->PixelFormat = PixelFormat;

// 判断边长是否是 2 的幂次方,如果当前的边长不是 2 的幂次方的时候,不开启 Mipmap 压缩
if ( (SizeX & (SizeX - 1) || (SizeY & (SizeY - 1))) )
Texture->MipGenSettings = TextureMipGenSettings::TMGS_NoMipmaps;

// 分配第一个 Mip
FTexture2DMipMap* Mip = new FTexture2DMipMap();
Texture->PlatformData->Mips.Add(Mip);
Mip->SizeX = SizeX;
Mip->SizeY = SizeY;

// 向 Mip 的 Bulkdata 中写入数据,注意先加上锁
Mip->BulkData.Lock(LOCK_READ_WRITE);
uint8* TextureData = (uint8*) Mip->BulkData.Realloc(PixelSize);
FMemory::Memcpy(TextureData, UncompressedRGBA->GetData(), PixelSize);
Mip->BulkData.Unlock();

// 初始化 Texture 的信息,完成数据的临时写入,也就是可选保存的状态
Texture->Source.Init(SizeX, SizeY, 1, 1, ETextureSourceFormat::TSF_BGRA8, UncompressedRGBA->GetData());
Texture->UpdateResource();
}

保存资源

1
2
3
4
5
6
7
8
9
// Create Assets
TexturePackage->MarkPackageDirty();
FAssetRegistryModule::AssetCreated(Texture);

// 获取 Package 的文件名
FString PackageFileName = FPackageName::LongPackageNameToFilename(PackageName, FPackageName::GetAssetPackageExtension());

// 保存资源
UPackage::SavePackage(TexturePackage, Texture, EObjectFlags::RF_Public | EObjectFlags::RF_Standalone, *PackageFileName);

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
UTexture2D* CreateTexture()
{
UTexture2D* Texture = nullptr;

const FString PicturePath = ImageDirectory.Path + TEXT("/") + ImageName + TEXT(".png");

if (!FPlatformFileManager::Get().GetPlatformFile().FileExists(*PicturePath))
{
return nullptr;
}

const FString PackageName = MountPointName + TEXT("/textures/") + ImageName;

// Read loacl PNG to Texture

TArray<uint8> RawFileData;
if (!FFileHelper::LoadFileToArray(RawFileData, *PicturePath))
{
return nullptr;
}

IImageWrapperModule& ImageWrapperModule = FModuleManager::LoadModuleChecked<IImageWrapperModule>("ImageWrapper");

TSharedPtr<IImageWrapper> ImageWrapper = ImageWrapperModule.CreateImageWrapper(EImageFormat::PNG);

if (ImageWrapper.IsValid() && ImageWrapper->SetCompressed(RawFileData.GetData(), RawFileData.Num()))
{
const TArray<uint8>* UncompressedRGBA = nullptr;
if (ImageWrapper->GetRaw(ERGBFormat::BGRA, 8, UncompressedRGBA))
{
int32 SizeX = ImageWrapper->GetWidth();
int32 SizeY = ImageWrapper->GetHeight();
const EPixelFormat PixelFormat = PF_B8G8R8A8;
int32 PixelSize = SizeX * SizeY * GPixelFormats[PixelFormat].BlockBytes;

if (SizeX > 0 && SizeY > 0 && (SizeX % GPixelFormats[PixelFormat].BlockSizeX) == 0 && (SizeY % GPixelFormats[PixelFormat].BlockSizeY) == 0)
{
// Create new texture pointer
UPackage* TexturePackage = CreatePackage(nullptr, *PackageName);
TexturePackage->FullyLoad();
Texture = NewObject<UTexture2D>(TexturePackage, FName(*ImageName), RF_Public | RF_Standalone | RF_MarkAsRootSet);

Texture->PlatformData = new FTexturePlatformData();
Texture->PlatformData->SizeX = SizeX;
Texture->PlatformData->SizeY = SizeY;
Texture->PlatformData->NumSlices = 1;
Texture->PlatformData->PixelFormat = PixelFormat;

// Determine whether it is a power of 2 to use mipmap
if ( (SizeX & (SizeX - 1) || (SizeY & (SizeY - 1))) )
Texture->MipGenSettings = TextureMipGenSettings::TMGS_NoMipmaps;

// Allocate first mipmap.
FTexture2DMipMap* Mip = new FTexture2DMipMap();
Texture->PlatformData->Mips.Add(Mip);
Mip->SizeX = SizeX;
Mip->SizeY = SizeY;

// Lock the texture so that it can be modified
Mip->BulkData.Lock(LOCK_READ_WRITE);
uint8* TextureData = (uint8*) Mip->BulkData.Realloc(PixelSize);
FMemory::Memcpy(TextureData, UncompressedRGBA->GetData(), PixelSize);
Mip->BulkData.Unlock();

Texture->Source.Init(SizeX, SizeY, 1, 1, ETextureSourceFormat::TSF_BGRA8, UncompressedRGBA->GetData());
Texture->UpdateResource();

// Create Assets
TexturePackage->MarkPackageDirty();

FAssetRegistryModule::AssetCreated(Texture);

FString PackageFileName = FPackageName::LongPackageNameToFilename(PackageName, FPackageName::GetAssetPackageExtension());

UPackage::SavePackage(TexturePackage, Texture, EObjectFlags::RF_Public | EObjectFlags::RF_Standalone, *PackageFileName);
}
}
}

return Texture;
}

参考源码

EditorFactories : UTextureFactory::ImportTexture():实现了从外部导入图片生成 Texture

Texture2D : UTexture2D::CreateTransient():实现了生成一个临时性的 Texture2D 文件

Texture : FTextureSource::GetMipData():实现了获取 Texture 中 Mip 的数据

Texture2D -> PNG

分析

  1. 修改 Texture 的设置
  2. 读取像素信息并生成图片

修改 Texture 的设置

在原本 Texture 的设置下,我们可能无法从 BulkData 读取出信息,会返回一个空指针,所以我们需要先修改一下 Texture 的设置;同时记录一下原本的设置,用于之后的复原。

CompressionSettings 设置为 TC_VectorDisplacementmap

MipGenSettings 设置为 TMGS_NoMipmaps

SRGB 设置为 false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 记录原本的设置信息
TextureCompressionSettings OldCompressionSettings = Texture->CompressionSettings;
TextureMipGenSettings OldMipGenSettings = Texture->MipGenSettings;
bool OldSRGB = Texture->SRGB;

// 修改设置
Texture->CompressionSettings = TextureCompressionSettings::TC_VectorDisplacementmap;
Texture->MipGenSettings = TextureMipGenSettings::TMGS_NoMipmaps;
Texture->SRGB = false;
Texture->UpdateResource();

/* 读取信息 */

// 复原回原本的设置
Texture->CompressionSettings = OldCompressionSettings;
Texture->MipGenSettings = OldMipGenSettings;
Texture->SRGB = OldSRGB;
Texture->UpdateResource();

读取像素信息并生成图片

我们从 Texture 的 Platform 中读取出 Mip 的 BulkData,然后对其进行映射,将信息映射到 PixelColor 中

image-20210812145950820

每一个 PixelColor 包含着 BGRA 的信息,多个 PixelColor 组成了数组 ColorData;

接着依赖于 ImageUtils ,我们压缩这个 ColorData 生成 ImageData,并将其写入指定位置,就可以生成出 png 图片。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// export texture to image
FTexture2DMipMap& Mip = Texture->PlatformData->Mips[0];
uint8* TextureData = (uint8*) Mip.BulkData.Lock(LOCK_READ_WRITE);
Mip.BulkData.Unlock();

int32 SizeX = Texture->PlatformData->SizeX;
int32 SizeY = Texture->PlatformData->SizeY;
TArray<FColor> ColorData;
for (int32 IndexY = 0; IndexY < SizeY; IndexY++)
{
for (int32 IndexX = 0; IndexX < SizeX; IndexX++)
{
FColor PixelColor;
PixelColor.B = TextureData[(IndexY * SizeX + IndexX) * 4 + 0];
PixelColor.G = TextureData[(IndexY * SizeX + IndexX) * 4 + 1];
PixelColor.R = TextureData[(IndexY * SizeX + IndexX) * 4 + 2];
PixelColor.A = TextureData[(IndexY * SizeX + IndexX) * 4 + 3];
ColorData.Add(PixelColor);
}
}

TArray<uint8> ImageData;
FImageUtils::CompressImageArray(SizeX, SizeY, ColorData, ImageData);
FFileHelper::SaveArrayToFile(ImageData, *PicturePath);

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
void WriteTexture(UTexture2D* Texture)
{
FString PicturePath = ExportImageDirectory.Path + TEXT("/") + Texture->GetName() + TEXT(".png");

// record old settings
TextureCompressionSettings OldCompressionSettings = Texture->CompressionSettings;
TextureMipGenSettings OldMipGenSettings = Texture->MipGenSettings;
bool OldSRGB = Texture->SRGB;

// modified to exportable settings
Texture->CompressionSettings = TextureCompressionSettings::TC_VectorDisplacementmap;
Texture->MipGenSettings = TextureMipGenSettings::TMGS_NoMipmaps;
Texture->SRGB = false;
Texture->UpdateResource();

// export texture to image
FTexture2DMipMap& Mip = Texture->PlatformData->Mips[0];
uint8* TextureData = (uint8*) Mip.BulkData.Lock(LOCK_READ_WRITE);
Mip.BulkData.Unlock();

int32 SizeX = Texture->PlatformData->SizeX;
int32 SizeY = Texture->PlatformData->SizeY;
TArray<FColor> ColorData;
for (int32 IndexY = 0; IndexY < SizeY; IndexY++)
{
for (int32 IndexX = 0; IndexX < SizeX; IndexX++)
{
FColor PixelColor;
PixelColor.B = TextureData[(IndexY * SizeX + IndexX) * 4 + 0];
PixelColor.G = TextureData[(IndexY * SizeX + IndexX) * 4 + 1];
PixelColor.R = TextureData[(IndexY * SizeX + IndexX) * 4 + 2];
PixelColor.A = TextureData[(IndexY * SizeX + IndexX) * 4 + 3];
ColorData.Add(PixelColor);
}
}

TArray<uint8> ImageData;
FImageUtils::CompressImageArray(SizeX, SizeY, ColorData, ImageData);
FFileHelper::SaveArrayToFile(ImageData, *PicturePath);

// return to old settings
Texture->CompressionSettings = OldCompressionSettings;
Texture->MipGenSettings = OldMipGenSettings;
Texture->SRGB = OldSRGB;
Texture->UpdateResource();
}

参考源码

GameViewportClient : UGameViewportClient::ProcessScreenShots():实现了导出屏幕截图

Sprite -> PNG

分析

首先我们来看一下 Sprite 的信息:

image-20210812150627183

其中,SourceUV 切割出的图片,左上角的偏移量;Source Dimension 表示切割图片的大小;SourceTexture 表示从哪个 Texture 切割而来。

举个例子:

image-20210812231527306

在这幅图中,Texture->PlatformData->SizeX = 9Texture->PlatformData->SizeY = 5

假设我们切割的是红色部分,则 SourceUV = (3, 1) SourceDimension = (4, 3),此时的偏移量应该是 9(第一行) + 3(第二行空白处) = 12

我们发现 PaperSprite 中预留了这个函数,可以让我们获取到这三个信息:

image-20210812150952019

这下子就很明确了,我们拿到这些信息,通过计算偏移量,就可以从 SourceTexture 中切割出图片来,剩下的内容和 Texture -> PNG 就一样了。

还一些需要注意的是,记得包含 PaperSpriteComponent.hPaperSprite.h,如果在插件中使用 Sprite,需要依赖模块 Paper2D

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

void WriteSprite(UPaperSprite* Sprite)
{
FString PicturePath = ExportImageDirectory.Path + TEXT("/") + Sprite->GetName() + TEXT(".png");

// record old settings
UTexture2D* Texture = Sprite->GetSourceTexture();

TextureCompressionSettings OldCompressionSettings = Texture->CompressionSettings;
TextureMipGenSettings OldMipGenSettings = Texture->MipGenSettings;
bool OldSRGB = Texture->SRGB;

// modified to exportable settings
Texture->CompressionSettings = TextureCompressionSettings::TC_VectorDisplacementmap;
Texture->MipGenSettings = TextureMipGenSettings::TMGS_NoMipmaps;
Texture->SRGB = false;
Texture->UpdateResource();

// export texture to image
FTexture2DMipMap& Mip = Texture->PlatformData->Mips[0];
uint8* TextureData = (uint8*) Mip.BulkData.Lock(LOCK_READ_WRITE);
Mip.BulkData.Unlock();

// 切割 Sprite,计算偏移量
FVector2D SourceUV = Sprite->GetSourceUV();
FVector2D SourceSize = Sprite->GetSourceSize();

int32 SizeX = (int32)SourceSize.X;
int32 SizeY = (int32)SourceSize.Y;
int32 Offsets = (int32)SourceUV.Y * Texture->PlatformData->SizeX + (int32)SourceUV.X;


TArray<FColor> ColorData;
for (int32 IndexY = 0; IndexY < SizeY; IndexY++)
{
for (int32 IndexX = 0; IndexX < SizeX; IndexX++)
{
FColor PixelColor;
PixelColor.B = TextureData[(IndexY * Texture->PlatformData->SizeX + IndexX + Offsets) * 4 + 0];
PixelColor.G = TextureData[(IndexY * Texture->PlatformData->SizeX + IndexX + Offsets) * 4 + 1];
PixelColor.R = TextureData[(IndexY * Texture->PlatformData->SizeX + IndexX + Offsets) * 4 + 2];
PixelColor.A = TextureData[(IndexY * Texture->PlatformData->SizeX + IndexX + Offsets) * 4 + 3];
ColorData.Add(PixelColor);
}
}

TArray<uint8> ImageData;
FImageUtils::CompressImageArray(SizeX, SizeY, ColorData, ImageData);
FFileHelper::SaveArrayToFile(ImageData, *PicturePath);

// return to old settings
Texture->CompressionSettings = OldCompressionSettings;
Texture->MipGenSettings = OldMipGenSettings;
Texture->SRGB = OldSRGB;
Texture->UpdateResource();
}